//
// KnownTypeCollection.cs
//
// Author:
//	Atsushi Enomoto <atsushi@ximian.com>
//
// Copyright (C) 2005 Novell, Inc.  http://www.novell.com
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
#if NET_2_0
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Xml;
using System.Xml.Schema;

using QName = System.Xml.XmlQualifiedName;
using System.Xml.Serialization;

namespace System.Runtime.Serialization
{
/*
	XmlFormatter implementation design inference:

	type definitions:
	- No XML Schema types are directly used. There are some maps from
	  xs:blahType to ms:blahType where the namespaceURI for prefix "ms" is
	  "http://schemas.microsoft.com/2003/10/Serialization/" .

	serializable types:
	- An object being serialized 1) must be of type System.Object, or
	  2) must be null, or 3) must have either a [DataContract] attribute
	  or a [Serializable] attribute to be serializable.
	- When the object is either of type System.Object or null, then the
	  XML type is "anyType".
	- When the object is [Serializable], then the runtime-serialization
	  compatible object graph is written.
	- Otherwise the serialization is based on contract attributes.
	  ([Serializable] takes precedence).

	type derivation:
	- For type A to be serializable, the base type B of A must be
	  serializable.
	- If a type which is [Serializable] and whose base type has a
	  [DataContract], then for base type members [DataContract] is taken.
	- It is vice versa i.e. if the base type is [Serializable] and the
	  derived type has a [DataContract], then [Serializable] takes place
	  for base members.

	known type collection:
	- It internally manages mapping store keyed by contract QNames.
	  KnownTypeCollection.Add() checks if the same QName contract already
	  exists (and raises InvalidOperationException if required).

*/
	internal static class TypeExtensions
	{
		public static T GetCustomAttribute<T> (this Type type, bool inherit)
		{
			var arr = type.GetCustomAttributes (typeof (T), inherit);
			return arr != null && arr.Length == 1 ? (T) arr [0] : default (T);
		}
	}

	internal sealed class KnownTypeCollection : Collection<Type>
	{
		internal const string MSSimpleNamespace =
			"http://schemas.microsoft.com/2003/10/Serialization/";
		internal const string MSArraysNamespace =
			"http://schemas.microsoft.com/2003/10/Serialization/Arrays";
		internal const string DefaultClrNamespaceBase =
			"http://schemas.datacontract.org/2004/07/";

		static QName any_type, bool_type,
			byte_type, date_type, decimal_type, double_type,
			float_type, string_type,
			short_type, int_type, long_type,
			ubyte_type, ushort_type, uint_type, ulong_type,
			// non-TypeCode
			any_uri_type, base64_type, duration_type, qname_type,
			// custom in ms nsURI schema
			char_type, guid_type,
			// not in ms nsURI schema
			dbnull_type;

		static KnownTypeCollection ()
		{
			//any_type, bool_type,	byte_type, date_type, decimal_type, double_type,	float_type, string_type,
			// short_type, int_type, long_type, 	ubyte_type, ushort_type, uint_type, ulong_type,
			// 	any_uri_type, base64_type, duration_type, qname_type,
			// 	char_type, guid_type,	dbnull_type;
			string s = MSSimpleNamespace;
			any_type = new QName ("anyType", s);
			any_uri_type = new QName ("anyURI", s);
			bool_type = new QName ("boolean", s);
			base64_type = new QName ("base64Binary", s);
			date_type = new QName ("dateTime", s);
			duration_type = new QName ("duration", s);
			qname_type = new QName ("QName", s);
			decimal_type = new QName ("decimal", s);
			double_type = new QName ("double", s);
			float_type = new QName ("float", s);
			byte_type = new QName ("byte", s);
			short_type = new QName ("short", s);
			int_type = new QName ("int", s);
			long_type = new QName ("long", s);
			ubyte_type = new QName ("unsignedByte", s);
			ushort_type = new QName ("unsignedShort", s);
			uint_type = new QName ("unsignedInt", s);
			ulong_type = new QName ("unsignedLong", s);
			string_type = new QName ("string", s);
			guid_type = new QName ("guid", s);
			char_type = new QName ("char", s);

			dbnull_type = new QName ("DBNull", MSSimpleNamespace + "System");
		}

		// FIXME: find out how QName and guid are processed

		internal QName GetXmlName (Type type)
		{
			SerializationMap map = FindUserMap (type);
			if (map != null)
				return map.XmlName;
			return GetPredefinedTypeName (type);
		}

		internal static QName GetPredefinedTypeName (Type type)
		{
			QName name = GetPrimitiveTypeName (type);
			if (name != QName.Empty)
				return name;
			if (type == typeof (DBNull))
				return dbnull_type;
			return QName.Empty;
		}

		internal static QName GetPrimitiveTypeName (Type type)
		{
			if (type.IsGenericType && type.GetGenericTypeDefinition () == typeof (Nullable<>))
				return GetPrimitiveTypeName (type.GetGenericArguments () [0]);

			if (type.IsEnum)
				return QName.Empty;

			switch (Type.GetTypeCode (type)) {
			case TypeCode.Object: // other than System.Object
			case TypeCode.DBNull: // it is natively mapped, but not in ms serialization namespace.
			case TypeCode.Empty:
			default:
				if (type == typeof (object))
					return any_type;
				if (type == typeof (Guid))
					return guid_type;
				if (type == typeof (TimeSpan))
					return duration_type;
				if (type == typeof (byte []))
					return base64_type;
				if (type == typeof (Uri))
					return any_uri_type;
				return QName.Empty;
			case TypeCode.Boolean:
				return bool_type;
			case TypeCode.Byte:
				return ubyte_type;
			case TypeCode.Char:
				return char_type;
			case TypeCode.DateTime:
				return date_type;
			case TypeCode.Decimal:
				return decimal_type;
			case TypeCode.Double:
				return double_type;
			case TypeCode.Int16:
				return short_type;
			case TypeCode.Int32:
				return int_type;
			case TypeCode.Int64:
				return long_type;
			case TypeCode.SByte:
				return byte_type;
			case TypeCode.Single:
				return float_type;
			case TypeCode.String:
				return string_type;
			case TypeCode.UInt16:
				return ushort_type;
			case TypeCode.UInt32:
				return uint_type;
			case TypeCode.UInt64:
				return ulong_type;
			}
		}

		internal static string PredefinedTypeObjectToString (object obj)
		{
			Type type = obj.GetType ();
			switch (Type.GetTypeCode (type)) {
			case TypeCode.Object: // other than System.Object
			case TypeCode.Empty:
			default:
				if (type == typeof (object))
					return String.Empty;
				if (type == typeof (Guid))
					return XmlConvert.ToString ((Guid) obj);
				if (type == typeof (TimeSpan))
					return XmlConvert.ToString ((TimeSpan) obj);
				if (type == typeof (byte []))
					return Convert.ToBase64String ((byte []) obj);
				if (type == typeof (Uri))
					return ((Uri) obj).ToString ();
				throw new Exception ("Internal error: missing predefined type serialization for type " + type.FullName);
			case TypeCode.DBNull: // predefined, but not primitive
				return String.Empty;
			case TypeCode.Boolean:
				return XmlConvert.ToString ((bool) obj);
			case TypeCode.Byte:
				return XmlConvert.ToString ((int)((byte) obj));
			case TypeCode.Char:
				return XmlConvert.ToString ((uint) (char) obj);
			case TypeCode.DateTime:
				return XmlConvert.ToString ((DateTime) obj, XmlDateTimeSerializationMode.RoundtripKind);
			case TypeCode.Decimal:
				return XmlConvert.ToString ((decimal) obj);
			case TypeCode.Double:
				return XmlConvert.ToString ((double) obj);
			case TypeCode.Int16:
				return XmlConvert.ToString ((short) obj);
			case TypeCode.Int32:
				return XmlConvert.ToString ((int) obj);
			case TypeCode.Int64:
				return XmlConvert.ToString ((long) obj);
			case TypeCode.SByte:
				return XmlConvert.ToString ((sbyte) obj);
			case TypeCode.Single:
				return XmlConvert.ToString ((float) obj);
			case TypeCode.String:
				return (string) obj;
			case TypeCode.UInt16:
				return XmlConvert.ToString ((int) (ushort) obj);
			case TypeCode.UInt32:
				return XmlConvert.ToString ((uint) obj);
			case TypeCode.UInt64:
				return XmlConvert.ToString ((ulong) obj);
			}
		}

		// FIXME: xsd types and ms serialization types should be differentiated.
		internal static Type GetPrimitiveTypeFromName (string name)
		{
			switch (name) {
			case "anyURI":
				return typeof (Uri);
			case "boolean":
				return typeof (bool);
			case "base64Binary":
				return typeof (byte []);
			case "dateTime":
				return typeof (DateTime);
			case "duration":
				return typeof (TimeSpan);
			case "QName":
				return typeof (QName);
			case "decimal":
				return typeof (decimal);
			case "double":
				return typeof (double);
			case "float":
				return typeof (float);
			case "byte":
				return typeof (sbyte);
			case "short":
				return typeof (short);
			case "int":
				return typeof (int);
			case "long":
				return typeof (long);
			case "unsignedByte":
				return typeof (byte);
			case "unsignedShort":
				return typeof (ushort);
			case "unsignedInt":
				return typeof (uint);
			case "unsignedLong":
				return typeof (ulong);
			case "string":
				return typeof (string);
			case "anyType":
				return typeof (object);
			case "guid":
				return typeof (Guid);
			case "char":
				return typeof (char);
			default:
				return null;
			}
		}


		internal static object PredefinedTypeStringToObject (string s,
			string name, XmlReader reader)
		{
			switch (name) {
			case "anyURI":
				return new Uri(s,UriKind.RelativeOrAbsolute);
			case "boolean":
				return XmlConvert.ToBoolean (s);
			case "base64Binary":
				return Convert.FromBase64String (s);
			case "dateTime":
				return XmlConvert.ToDateTime (s, XmlDateTimeSerializationMode.RoundtripKind);
			case "duration":
				return XmlConvert.ToTimeSpan (s);
			case "QName":
				int idx = s.IndexOf (':');
				string l = idx < 0 ? s : s.Substring (idx + 1);
				return idx < 0 ? new QName (l) :
					new QName (l, reader.LookupNamespace (
						s.Substring (0, idx)));
			case "decimal":
				return XmlConvert.ToDecimal (s);
			case "double":
				return XmlConvert.ToDouble (s);
			case "float":
				return XmlConvert.ToSingle (s);
			case "byte":
				return XmlConvert.ToSByte (s);
			case "short":
				return XmlConvert.ToInt16 (s);
			case "int":
				return XmlConvert.ToInt32 (s);
			case "long":
				return XmlConvert.ToInt64 (s);
			case "unsignedByte":
				return XmlConvert.ToByte (s);
			case "unsignedShort":
				return XmlConvert.ToUInt16 (s);
			case "unsignedInt":
				return XmlConvert.ToUInt32 (s);
			case "unsignedLong":
				return XmlConvert.ToUInt64 (s);
			case "string":
				return s;
			case "guid":
				return XmlConvert.ToGuid (s);
			case "anyType":
				return s;
			case "char":
				return (char) XmlConvert.ToUInt32 (s);
			default:
				throw new Exception ("Unanticipated primitive type: " + name);
			}
		}

		List<SerializationMap> contracts = new List<SerializationMap> ();

		public KnownTypeCollection ()
		{
		}

		protected override void ClearItems ()
		{
			base.Clear ();
		}

		protected override void InsertItem (int index, Type type)
		{
			if (TryRegister (type))
				base.InsertItem (index, type);
		}

		// FIXME: it could remove other types' dependencies.
		protected override void RemoveItem (int index)
		{
			Type t = base [index];
			List<SerializationMap> l = new List<SerializationMap> ();
			foreach (SerializationMap m in contracts) {
				if (m.RuntimeType == t)
					l.Add (m);
			}
			foreach (SerializationMap m in l) {
				contracts.Remove (m);
				base.RemoveItem (index);
			}
		}

		protected override void SetItem (int index, Type type)
		{
			if (index == Count)
				InsertItem (index, type);
			else {
				RemoveItem (index);
				if (TryRegister (type))
					base.InsertItem (index - 1, type);
			}
		}

		internal SerializationMap FindUserMap (QName qname)
		{
			for (int i = 0; i < contracts.Count; i++)
				if (qname == contracts [i].XmlName)
					return contracts [i];
			return null;
		}

		internal Type GetSerializedType (Type type)
		{
			Type element = GetCollectionElementType (type);
			if (element == null)
				return type;
			QName name = GetQName (type);
			var map = FindUserMap (name);
			if (map != null)
				return map.RuntimeType;
			return type;
		}

		internal SerializationMap FindUserMap (Type type)
		{
			for (int i = 0; i < contracts.Count; i++)
				if (type == contracts [i].RuntimeType)
					return contracts [i];
			return null;
		}

		internal QName GetQName (Type type)
		{
			if (IsPrimitiveNotEnum (type))
				return GetPrimitiveTypeName (type);

			SerializationMap map = FindUserMap (type);
			if (map != null)
				// already mapped.
				return map.XmlName; 

			if (type.IsEnum)
				return GetEnumQName (type);

			QName qname = GetContractQName (type);
			if (qname != null)
				return qname;

			if (type.GetInterface ("System.Xml.Serialization.IXmlSerializable") != null)
				//FIXME: Reusing GetSerializableQName here, since we just 
				//need name of the type..
				return GetSerializableQName (type);

			qname = GetCollectionContractQName (type);
			if (qname != null)
				return qname;

			Type element = GetCollectionElementType (type);
			if (element != null)
				return GetCollectionQName (element);

			if (GetAttribute<SerializableAttribute> (type) != null)
				return GetSerializableQName (type);

			// FIXME: it needs in-depth check.
			return QName.Empty;
		}

		QName GetContractQName (Type type)
		{
			var a = GetAttribute<DataContractAttribute> (type);
			return a == null ? null : GetContractQName (type, a.Name, a.Namespace);
		}

		QName GetCollectionContractQName (Type type)
		{
			var a = GetAttribute<CollectionDataContractAttribute> (type);
			return a == null ? null : GetContractQName (type, a.Name, a.Namespace);
		}

		internal static QName GetContractQName (Type type, string name, string ns)
		{
			if (name == null) {
				// FIXME: there could be decent ways to get 
				// the same result...
				name = type.Namespace == null || type.Namespace.Length == 0 ? type.Name : type.FullName.Substring (type.Namespace.Length + 1).Replace ('+', '.');
				if (type.IsGenericType) {
					name = name.Substring (0, name.IndexOf ('`')) + "Of";
					foreach (var t in type.GetGenericArguments ())
						name += t.Name; // FIXME: check namespaces too
				}
			}
			if (ns == null)
				ns = DefaultClrNamespaceBase + type.Namespace;
			return new QName (name, ns);
		}

		private QName GetEnumQName (Type type)
		{
			string name = null, ns = null;

			if (!type.IsEnum)
				return null;

			var dca = GetAttribute<DataContractAttribute> (type);

			if (dca != null) {
				ns = dca.Namespace;
				name = dca.Name;
			}

			if (ns == null)
				ns = DefaultClrNamespaceBase + type.Namespace;

			if (name == null)
				name = type.Namespace == null ? type.Name : type.FullName.Substring (type.Namespace.Length + 1).Replace ('+', '.');

			return new QName (name, ns);
		}

		private QName GetCollectionQName (Type element)
		{
			QName eqname = GetQName (element);
			
			string ns = eqname.Namespace;
			if (eqname.Namespace == MSSimpleNamespace)
				//Arrays of Primitive types
				ns = MSArraysNamespace;

			return new QName (
				"ArrayOf" + XmlConvert.EncodeLocalName (eqname.Name),
				ns);
		}

		private QName GetSerializableQName (Type type)
		{
			string xmlName = type.Name;
			if (type.IsGenericType) {
				xmlName = xmlName.Substring (0, xmlName.IndexOf ('`')) + "Of";
				foreach (var t in type.GetGenericArguments ())
					xmlName += GetQName (t).Name; // FIXME: check namespaces too
			}
			string xmlNamespace = DefaultClrNamespaceBase + type.Namespace;
			var x = GetAttribute<XmlRootAttribute> (type);
			if (x != null) {
				xmlName = x.ElementName;
				xmlNamespace = x.Namespace;
			}
			return new QName (XmlConvert.EncodeLocalName (xmlName), xmlNamespace);
		}

		internal bool IsPrimitiveNotEnum (Type type)
		{
			if (type.IsEnum)
				return false;
			if (Type.GetTypeCode (type) != TypeCode.Object) // explicitly primitive
				return true;
			if (type == typeof (Guid) || type == typeof (object) || type == typeof(TimeSpan) || type == typeof(byte[]) || type==typeof(Uri)) // special primitives
				return true;
			// nullable
			if (type.IsGenericType && type.GetGenericTypeDefinition () == typeof (Nullable<>))
				return IsPrimitiveNotEnum (type.GetGenericArguments () [0]);
			return false;
		}

		internal bool TryRegister (Type type)
		{
			// exclude predefined maps
			if (IsPrimitiveNotEnum (type))
				return false;

			if (FindUserMap (type) != null)
				return false;

			if (RegisterEnum (type) != null)
				return true;

			if (RegisterContract (type) != null)
				return true;

			if (RegisterIXmlSerializable (type) != null)
				return true;

			if (RegisterDictionary (type) != null)
				return true;

			if (RegisterCollectionContract (type) != null)
				return true;

			if (RegisterCollection (type) != null)
				return true;

			if (GetAttribute<SerializableAttribute> (type) != null) {
				RegisterSerializable (type);
				return true;
			}

			RegisterDefaultTypeMap (type);
			return true;
		}

		internal static Type GetCollectionElementType (Type type)
		{
			if (type.IsArray)
				return type.GetElementType ();

			Type [] ifaces = type.GetInterfaces ();
			foreach (Type i in ifaces)
				if (i.IsGenericType && i.GetGenericTypeDefinition ().Equals (typeof (ICollection<>)))
					return i.GetGenericArguments () [0];
			foreach (Type i in ifaces)
				if (i == typeof (IList))
					return typeof (object);
			return null;
		}

		internal T GetAttribute<T> (MemberInfo mi) where T : Attribute
		{
			object [] atts = mi.GetCustomAttributes (typeof (T), false);
			return atts.Length == 0 ? null : (T) atts [0];
		}

		private CollectionContractTypeMap RegisterCollectionContract (Type type)
		{
			var cdca = GetAttribute<CollectionDataContractAttribute> (type);
			if (cdca == null)
				return null;

			Type element = GetCollectionElementType (type);
			if (element == null)
				throw new InvalidOperationException (String.Format ("Type '{0}' is marked as collection contract, but it is not a collection", type));

			TryRegister (element); // must be registered before the name conflict check.

			QName qname = GetCollectionContractQName (type);
			CheckStandardQName (qname);
			if (FindUserMap (qname) != null)
				throw new InvalidOperationException (String.Format ("Failed to add type {0} to known type collection. There already is a registered type for XML name {1}", type, qname));

			var ret = new CollectionContractTypeMap (type, cdca, element, qname, this);
			contracts.Add (ret);
			return ret;
		}

		private CollectionTypeMap RegisterCollection (Type type)
		{
			Type element = GetCollectionElementType (type);
			if (element == null)
				return null;

			TryRegister (element);

			QName qname = GetCollectionQName (element);

			var map = FindUserMap (qname);
			if (map != null) {
				var cmap = map as CollectionTypeMap;
				if (cmap == null || cmap.RuntimeType != type)
					throw new InvalidOperationException (String.Format ("Failed to add type {0} to known type collection. There already is a registered type for XML name {1}", type, qname));
				return cmap;
			}

			CollectionTypeMap ret =
				new CollectionTypeMap (type, element, qname, this);
			contracts.Add (ret);
			return ret;
		}

		static bool TypeImplementsIDictionary (Type type)
		{
			foreach (var iface in type.GetInterfaces ())
				if (iface == typeof (IDictionary) || (iface.IsGenericType && iface.GetGenericTypeDefinition () == typeof (IDictionary<,>)))
					return true;

			return false;
		}

		// it also supports contract-based dictionary.
		private DictionaryTypeMap RegisterDictionary (Type type)
		{
			if (!TypeImplementsIDictionary (type))
				return null;

			var cdca = GetAttribute<CollectionDataContractAttribute> (type);

			DictionaryTypeMap ret =
				new DictionaryTypeMap (type, cdca, this);

			if (FindUserMap (ret.XmlName) != null)
				throw new InvalidOperationException (String.Format ("Failed to add type {0} to known type collection. There already is a registered type for XML name {1}", type, ret.XmlName));
			contracts.Add (ret);

			TryRegister (ret.KeyType);
			TryRegister (ret.ValueType);

			return ret;
		}

		private SerializationMap RegisterSerializable (Type type)
		{
			QName qname = GetSerializableQName (type);

			if (FindUserMap (qname) != null)
				throw new InvalidOperationException (String.Format ("There is already a registered type for XML name {0}", qname));

			SharedTypeMap ret =
				new SharedTypeMap (type, qname, this);
			contracts.Add (ret);
			return ret;
		}

		private SerializationMap RegisterIXmlSerializable (Type type)
		{
			if (type.GetInterface ("System.Xml.Serialization.IXmlSerializable") == null)
				return null;

			QName qname = GetSerializableQName (type);

			if (FindUserMap (qname) != null)
				throw new InvalidOperationException (String.Format ("There is already a registered type for XML name {0}", qname));

			XmlSerializableMap ret = new XmlSerializableMap (type, qname, this);
			contracts.Add (ret);

			return ret;
		}

		void CheckStandardQName (QName qname)
		{
			switch (qname.Namespace) {
			case XmlSchema.Namespace:
			case XmlSchema.InstanceNamespace:
			case MSSimpleNamespace:
			case MSArraysNamespace:
				throw new InvalidOperationException (String.Format ("Namespace {0} is reserved and cannot be used for user serialization", qname.Namespace));
			}

		}

		private SharedContractMap RegisterContract (Type type)
		{
			QName qname = GetContractQName (type);
			if (qname == null)
				return null;
			CheckStandardQName (qname);
			if (FindUserMap (qname) != null)
				throw new InvalidOperationException (String.Format ("There is already a registered type for XML name {0}", qname));

			SharedContractMap ret = new SharedContractMap (type, qname, this);
			contracts.Add (ret);
			ret.Initialize ();

			object [] attrs = type.GetCustomAttributes (typeof (KnownTypeAttribute), true);
			for (int i = 0; i < attrs.Length; i++) {
				KnownTypeAttribute kt = (KnownTypeAttribute) attrs [i];
				TryRegister (kt.Type);
			}

			return ret;
		}

		DefaultTypeMap RegisterDefaultTypeMap (Type type)
		{
			DefaultTypeMap ret = new DefaultTypeMap (type, this);
			contracts.Add (ret);
			return ret;
		}

		private EnumMap RegisterEnum (Type type)
		{
			QName qname = GetEnumQName (type);
			if (qname == null)
				return null;

			if (FindUserMap (qname) != null)
				throw new InvalidOperationException (String.Format ("There is already a registered type for XML name {0}", qname));

			EnumMap ret =
				new EnumMap (type, qname, this);
			contracts.Add (ret);
			return ret;
		}
	}
}
#endif
